/**
 * 深度合并
 */

const hasOwnProperty = Object.prototype.hasOwnProperty;

function hasOwn(obj: any, key: PropertyKey): boolean {
    return hasOwnProperty.call(obj, key);
}

function makeMap(list: Array<PropertyKey>) {
    const map = Object.create(null);
    for (let i = 0; i < list.length; i++) {
        map[list[i]] = true;
    }
    return (val: string | number) => !!map[val] as boolean;
}

const objectToString = Object.prototype.toString;
function isPlainObject(val: any) {
    return objectToString.call(val) === '[object Object]';
}

function isPrimitive(val: any) {
    return (
        val === null ||
        typeof val === 'string' ||
        typeof val === 'number' ||
        typeof val === 'bigint' ||
        typeof val === 'symbol' ||
        typeof val === 'boolean' ||
        typeof val === 'undefined'
    );
}

// 数组去重，不保证顺序
function unique<T>(list: Array<T>) {
    const res: Array<T> = [];
    for (let i = 0, len = list.length; i < len; i++) {
        for (let j = i + 1; j < len; j++) {
            if (list[i] === list[j]) {
                j = ++i;
            }
        }
        res.push(list[i]);
    }
    return res;
}

function deepMerge<K, T>(
    o: K,
    n: T,
    dp?: boolean,
    ignores?: Array<string>,
) {
    const leftRecord = new WeakMap();
    const rightRecord = new WeakMap();
    const valueRecord = new WeakMap();
    const ignoresMap = makeMap(ignores || []);

    const isArray = Array.isArray;
    const isAllRefs = (a: object, b: object) => {
        if (leftRecord.has(a) || rightRecord.has(a)) {
            return leftRecord.has(b) || rightRecord.has(b);
        }
        return false
    };

    const clone = (v: any) => {
        if (isPrimitive(v) || typeof v === 'function') {
            return v;
        } else if (valueRecord.has(v)) {
            return valueRecord.get(v);
        } else if (leftRecord.has(v)) {
            return leftRecord.get(v);
        } else if (rightRecord.has(v)) {
            return rightRecord.get(v);
        } else if (isArray(v)) {
            if (dp) v = unique(v);
            const arr: any[] = [];
            valueRecord.set(v, arr);
            for (let i = 0, len = v.length; i < len; i++) {
                arr[i] = clone(v[i]);
            }
            return arr;
        } else if (typeof v === 'object') {
            const obj: Record<PropertyKey, any> = {};
            valueRecord.set(v, obj);
            const keys = Reflect.ownKeys(v);
            keys.forEach((key) => (obj[key] = clone(v[key])));
            return obj;
        }
    };

    const setValue = (r: WeakMap<object, any>, k: any, key: string | number | symbol) => {
        if (r.has(k)) {
            return r.get(k);
        } else {
            if (ignoresMap(key as string)) {  
                return k;
            }
            const val = clone(k);
            if (!isPrimitive(val) && typeof val !== 'function') {
                r.set(k, val);
            }
            return val;
        }
    };

    const mergeObject = (l: any, r: any) => {
        const res: Record<PropertyKey, any> = {};
        const leftKeys = Reflect.ownKeys(l);
        const rightKeys = Reflect.ownKeys(r);

        leftRecord.set(l, res);
        rightRecord.set(r, res);

        leftKeys.forEach((key) => {
            const lv = l[key];
            const rv = r[key];

            if (hasOwn(r, key)) {
                if (isArray(lv) && isArray(rv)) {
                    const item = clone([...lv, ...rv]);
                    res[key] = dp ? unique(item) : item;
                } else if (isPlainObject(lv) && isPlainObject(rv)) {
                    res[key] = isAllRefs(lv, rv)
                        ? leftRecord.get(lv)
                        : mergeObject(lv, rv);
                } else {
                    res[key] = setValue(rightRecord, rv, key);
                }
            } else {
                res[key] = setValue(leftRecord, lv, key);
            }
        });

        rightKeys.forEach((key) => {
            if (hasOwn(res, key)) return;
            res[key] = setValue(rightRecord, r[key], key);
        });

        return res;
    };

    return mergeObject(o, n) as K & T;
}

export default deepMerge;